#!/usr/bin/python

"""
File:       nlg.py

Class:      CMPS 140 Winter 2011
Project:    Papa Bot
Date:       01/27/11
Authors:    Jason Erickson
            Morgan Davis
            DJ Visbal
            Constantine Perepelitsa
            Jae Kim
            Huu Huynh
"""

import sys
import os
import string
from time import sleep
from random import seed, uniform
import db
import pickle
import operator

class NLG(object):
    """Natural Language Generator

    The NLG prints output to the user."""

    def __init__(self,debug_mode=False,db=None):
        self.debug_mode = debug_mode
        self.db = db
        seed()  # seed random # generator with system time

    def clear_screen(self):
        """Try to clear the screen."""
        if os.name == "posix":
            os.system("clear")
        elif os.name == "dos":
            os.system("cls")
        else:
            print "########################\n"
            
    def print_drink_names(self,drink_ids,user,n=10):
        '''Prints a list of the first n given drinks, minus those the user doesn't like'''
        
         # remove drinks the user dislikes
        disliked_drinks = self.db.getUserDislikedDrinkIDs(user.id)

        # unfortunitely there is no really clean way to take the difference of these lists
        # one is sorted (can't convert to set). but this works.
        # note: assumes the list is set-like in that no drinkIDs are duplicated
        final_drinks = []
        for d in drink_ids:
            if not d in disliked_drinks:
                final_drinks.append(d)

        # get the top 10 and print their names
        ten_names = []
        dcount = 0
        for d in final_drinks:
            if dcount < n:
                ten_names.append(self.db.getDrinkName(d))
                dcount += 1
        out = []
        for name in ten_names:
            out.append(name)
        self.slow_print("\n".join(out))
        
    def print_drink_names_by_rating(self,drink_ids,user,n=10):
        '''Prints a list of the first n given drinks,
        sorted by webtender rating and with user-disliked drinks removed.
        
        drinks_ratings is a list of tuples of the form (drinkID,rating)'''
        
        # THIS IS SLOW
        # well, not really, just inefficient.
        drinks_ratings = []
        for d in drink_ids:
            drinks_ratings.append((d,self.db.getRating(d)))
        
        # sort the drinks by rating, and then remove the ratings from the list
        sorted_drinks_ratings = sorted(drinks_ratings,key=lambda d: d[1],reverse=True)
        sorted_drinks = [d[0] for d in sorted_drinks_ratings]
        
        self.print_drink_names(sorted_drinks,user,n)
        
    def ask_want_preferred_drink(self,user,drink):
        """Ask the user if they want a preferred (rated positively) drink"""
        # (ASSUMES user and drink are not None)
        firstDrinkLetter = drink[:1].lower()
        vowles = ['a','e','i','o','u']
        a = 'a'
        if firstDrinkLetter in vowles: a = 'an'
        self.slow_print("Howdy %s, can I get you %s %s?" % (user.name,a,drink))

    def recommend_drink(self,drink):
        """Ask the user if they want a drink"""
        # (ASSUMES user and drink are not None)
        firstDrinkLetter = drink[:1].lower()
        vowles = ['a','e','i','o','u']
        a = 'a'
        if firstDrinkLetter in vowles: a = 'an'
        self.slow_print("How about %s %s?" % (a,drink))

    def print_recipe(self, drink, ingredients, instructions):
        """Print a recipe, with ingredients and instructions."""
        # sent = []
        # sent.append("Okay, for a %s, you need " % (drink))
        # counter = 0
        # for ingredient in ingredients:
        #     if counter == len(ingredients)-1: # on last ingredient
        #         sent.append("and " + ingredient + ".\n")
        #     else:
        #         sent.append(ingredient + ", ")
        #     counter += 1
        # sent.append(instructions)
        # self.slow_print("".join(sent))
        
        self.slow_print("Okay, for a %s you need:\n" % (drink))
        for i in ingredients:
            self.slow_print(i)
        self.slow_print("\n"+instructions+"\n")

    def print_drink_names_containing(self, ingredients, user):
        """Print a list of drink names with a related ingredient"""
        if self.debug_mode:
            print >>sys.stderr, \
                "print_drink_names_containing(\n\t%s\n)" % repr(ingredients)
        # pretty-print ingredients
        names = None
        if len(ingredients) == 1:
            names = ingredients[0]
        else:
            names = ', '.join(ingredients[:-1])
            names += (',' if len(ingredients) > 2 else '') + ' and ' \
                     + ingredients[-1]
        self.slow_print("\nOk, let me see what I can do with %s..." % (names))
        
        # find drinks that contain all ingredients
        ingredientids = [self.db.getIngredientID(i) for i in ingredients]
        all_drinks = set( self.db.getDrinkIDsContaining(ingredientids[0]) )
        for i_id in ingredientids[1:]:
            dset = set( self.db.getDrinkIDsContaining(i_id) )
            all_drinks = all_drinks.intersection(dset)
            
        self.look_busy()
        if len(all_drinks) == 0:
            self.slow_print("I'm sorry, I don't think I can make any drinks "\
                            + "with all those ingredients.")
            return
            
        self.slow_print("\nI could make:\n")
        self.print_drink_names_by_rating(all_drinks,user)
        self.slow_print("\nWhich would you like?")
    
    def print_drink_names_withonly(self, user):
        """Used in cabinet mode -- assumes only these ingredients are 
        available and print drinks that can be made with nothing more."""
        
        ingredientids = user.get_cabinet()
        # pretty-print ingredients
        ingr_list = []
        ##List comprehensions save work and for loops -- also faster
        ingr_list = [self.db.getIngredientName(i_id) for i_id in ingredientids]
        names = None
        if len(ingr_list) == 1:
            names = ingr_list[0]
        else:
            names = ', '.join(ingr_list[:-1])
            names += (',' if len(ingr_list) > 2 else '') + ' and ' \
                     + ingr_list[-1]
        self.slow_print("\nHmmm... let me see what drinks I can make with %s..." % (names))
        
        can_make = []
        allDrinks = self.db.getAllDrinkIDs()
        drink_ingredients = pickle.load(open('drink_ingredients.pickle'))
        for drink in allDrinks:
            dIngreds = drink_ingredients[drink]
            for dIngred in dIngreds:
                if dIngred not in ingredientids:
                    break
            else:
                can_make.append(drink)

        if len(can_make) == 0:
            self.slow_print("I'm sorry, I can't make any drinks with only those ingredients.")
            return False
            
        # this method doesn't compute drinks that can be made correctly....
        # (we want to frind all drinks who's ingredients are a subset of those in the cabinet.)
        # # table of all drinks using each ingredient in user's cabinet
        # # eg. list of set of drinks using each ingredient
        # related_drink_table = [set(self.db.getDrinkIDsContaining(i)) \
        #                       for i in cabinet_ingredient_ids]
        # all_drink_ids = self.db.getAllDrinkIDs()
        # drink_ids = []
        # # reduce the number of lookups based on the fact that we only need to
        # # do a lookup if a drink uses at least two ingredients in the cabinet
        # for drink_id in all_drink_ids:
        #     c = 0
        #     for related_drinks in related_drink_table:
        #         if drink_id in related_drinks:
        #             c = c + 1
        #     if c >= 2:
        #         drink_ids.append(i)
        # 
        # # table of drinks and their ingredients
        # drink_ingredient_table = [(d, set(self.db.getIngredientIDs(d))) \
        #                          for d in drink_ids]
        # c = set(cabinet_ingredient_ids)
        # can_make = filter(lambda d: set(d[1]).issubset(c), \
        #                   drink_ingredient_table)
        # can_make = map(lambda d: d[0], can_make)
        # drink_names = [self.db.getDrinkName(drink) for drink in can_make]
        # 
        # if len(drink_names) == 0:
        #     self.slow_print("I'm sorry, I can't make any drinks with only those ingredients.")
        #     return
        
        self.slow_print("\nI could make:\n")
        self.print_drink_names_by_rating(can_make,user)
        self.slow_print("\nWhich would you like?")
        return True

    def print_recommended_drinks(self, userid, n=5):
        # simple rating system for now:
        # each appearance of an ingredient in a drink the user likes
        # increases its rating by 1
        # thereafter, every drink that includes at least one ingredient
        # the user says he prefers is rated by the sum of the ratings of
        # the ingredients in it
        # the n drinks with the highest ratings are returned
        # 
        # different rating schemes:
        # * ingredient rating as an average? i.e. the number of times it
        #   appears in a drink the user prefers? (is this mathematically
        #   equivalent?)
        # * change rating algorithm (i.e. from "sum ratings") to use
        #   amount of each ingredient as well; would probably require
        #   unit conversion (ew)
        
        # if user is None, then he has no liked drinks to work with
        # not that this routine can crash if n is too big.
        # DB.getBestRating() finds the highest rating of all drinks (10) and then returns
        # all drinks with rating 10. if there are < n drinks with rating 10, 
        # 'for name, rating in self.db.getBestRating()[:n]:' will crash.
        # it would be better to get a list of all (drink,rating), sort it, and slice it
        if userid is None:
            self.slow_print("\nHmm... I can't really recommend you "+\
                            "anything before I know what you like.")
            self.slow_print("But I'll tell you what -- here are a few"+\
                            " drinks some other folks have enjoyed:\n")
            for name, rating in self.db.getBestRating()[:n]:
                self.slow_print(name)
            return
        
        # build a dict of preferred ingredients: { ingr => rating }
        ingr_prefs = {}
        # a set of user-preferred drinks will be useful later:
        drinks_pref = set( self.db.getUserLikedDrinks(userid) )
        for drink_id in drinks_pref:
            for ingr_id in self.db.getIngredientIDs(drink_id):
                if not ingr_prefs.has_key(ingr_id):
                    ingr_prefs[ingr_id] = 1
                else:
                    ingr_prefs[ingr_id] += 1
        if self.debug_mode:
            print "total ingredients liked: " + str(len(ingr_prefs))
            liked_ingr__ids_sorted = (i[0] for i in sorted(ingr_prefs.iteritems(),key=operator.itemgetter(1),reverse=True))
            print liked_ingr__ids_sorted
            print "ingredient ranks: "
            for id in liked_ingr__ids_sorted:
                print "%s: %d" % (self.db.getIngredientName(id),ingr_prefs[id])       
        
        # get all drinks that could possibly be made from these ingredients
        potential_drinks = set()
        for ingr_id in ingr_prefs.keys():
            new_drinks = set( self.db.getDrinkIDsContaining(ingr_id) )
            potential_drinks = potential_drinks.union(new_drinks)
        # remove drinks the user already likes
        potential_drinks -= drinks_pref
        if self.debug_mode: print "total drinks with liked ingredient: " + str(len(potential_drinks))
        
        # rate potential drinks by user's preference in ingredients
        scores = []
        # NOTE: this pickle is from the database. !it is redundant! -- used to speed up computation
        drink_ingredients = pickle.load(open('drink_ingredients.pickle'))
        for drink_id in potential_drinks:
            score = reduce(lambda x,y: x + ingr_prefs.get(y, 0), \
                           drink_ingredients[drink_id], 0)
                           # preferrable, but 6K SQL lookups is costly:
                           # self.db.getIngredientIDs(drink_id), 0)
            scores.append( (drink_id, score) )
        
        # print out the drink names with the top n scores
        scores = sorted(scores, key=lambda score: score[1], reverse=True)
        self.slow_print("\nI think you might like some of these:\n")
        for drink_id in [s[0] for s in scores[:n]]:
            liked_ids = [i for i in drink_ingredients[drink_id] \
                               if i in ingr_prefs]
            liked_ids = sorted(liked_ids, \
                               key=lambda x: ingr_prefs.get(x, 0), \
                               reverse=True)[:3]
            liked_str = self.db.getIngredientName(liked_ids[0])
            for i, ingr_id in enumerate(liked_ids[1:]):
                if i == len(liked_ids[1:]) - 1:
                    liked_str += ', and '
                else:
                    liked_str += ', '
                liked_str += self.db.getIngredientName(ingr_id)
            self.slow_print("%s (contains %s)" \
                            % (self.db.getDrinkName(drink_id), liked_str))
                            
        self.slow_print("\nWhich would you like to try?")

    def print_drinks_for_properties(self,drinkIDs,properties,user):
        '''Print a list emphasizing the criteria they specified for'''
        prop_desc = []
        if "POPULAR" in properties:
            prop_desc.append("popular")
        if "STRONG" in properties:
            prop_desc.append("strong")
        sent = []
        sent.append("\nI can think of a few ")
        counter = 0
        for p in prop_desc:
            if counter == len(prop_desc)-1: # on last property
                sent.append("and " + p + " drinks:\n")
            elif counter == len(prop_desc)-2 and len(properties)<1: # on second-to-last property
                sent.append(p + " ")
            else:
                sent.append(p + ", ")
            counter += 1
        self.slow_print("".join(sent))
        self.print_drink_names(drinkIDs,user)
        self.slow_print("\nWhich would you like?")

    def ask_for_task(self,user):
        """Ask the user what he wants to do"""
        self.slow_print("Howdy %s, what can I do for you today?" % (user.name))
        
    def ask_for_drink(self):
        """Ask the user for a drink."""
        self.slow_print("Which drink would you like?")
        
    def ask_for_drink_in_category(self,catName,drink_ids,user):
        """Ask for a drink in a category"""
        # we need a plural version of the category name
        # i'm sure nltk does this, but since we only have 10 categories,
        # we can justdo this -- look at and replace the last letter in the category
        # z -> es
        # r -> rs
        # a -> as
        # h -> hes
        # i - >ies
        # e -> es
        pluralEndings = { "z":"zes", "r":"rs", "a":"as", "h":"hes", "i":"ies", "e":"es" }
        lastLetter = catName[-1:]
        if lastLetter in pluralEndings:
           pluralEnd = pluralEndings[lastLetter]
        else: # fall back to singular form
            pluralEnd = lastLetter
        pluralCat = catName[:len(catName)-1] + pluralEnd
        pluralCat = string.capwords(pluralCat)
        
        self.slow_print("I know how to make several " + pluralCat + ":\n")
        self.print_drink_names_by_rating(drink_ids,user)
        self.slow_print("\nWhich would you like?")

    def ask_for_ingredient(self, user):
        """Ask the user for an ingredient."""
        print ""
        self.slow_print( ("Okay, which ingredients, %s?") % (user.name))

    def affirmative(self):
        """Generic affirmative responce"""
        self.slow_print("Alright then.")

    def ask_cab_action(self, user):
        """Ask the user what he/she wants to do in cabinet mode"""
        print ""
        self.slow_print(("Wow, that is a nice cabinet you got there, %s!") % (user.name) )
        self.slow_print("You can add or remove ingredients from your cabinet...")
        print ""
        self.slow_print("What would you like to do?")

    def ask_anything_else(self,user):
        """Ask the user if he/she wants to do anything else."""
        self.slow_print("\nAnything else, %s?" % (user.name))

    def ask_user_preference(self,drink):
        """Ask the user to rate a drink they just tried"""
        self.slow_print("\nDo you like your %s?" % (drink))

    def ask_username(self):
        """Ask the user to identify himself."""
        self.slow_print("Hi, what's your name?")
                        
    def glad(self):
        """You're glad for the user's response."""
        self.slow_print("Glad to hear it.")
        
    def sorry(self):
        """You're sorry for the user's response."""
        self.slow_print("Oh, I'm sorry to hear that.")

    def ask_cabinet_action(self,name):
        """Ask the user what he wants to do with his cabinet."""
        print ""
        self.slow_print( ("Okay %s, what would you like to do with your cabinet?") %s (name) )

    def show_cabinet(self, ingredient_ids, justmodified=False):
        """Display the contents of the user's cabinet."""
        # justmodified indicates that the user just added/removed something
        if justmodified:
            self.slow_print("\nNow your cabinet contains:")
        else:
            self.slow_print("\nYour cabinet contains:")
        for id in ingredient_ids:
            self.slow_print(self.db.getIngredientName(id))

    def adding_to_cabinet(self, ingrs):
        """Reiterates the ingredients to be added to the user's cabinet."""
        sentence = ["Alright I'll add"]
        counter = 0
        for i_id in ingrs:
            counter += 1
            if (counter == len(ingrs)) and (len(ingrs) is not 1):
                sentence.append("and")
            if (counter is not len(ingrs)-1) and (counter is not len(ingrs)):
                sentence.append( ("%s,") % (self.db.getIngredientName(i_id)) )
            else:
                sentence.append(self.db.getIngredientName(i_id))
        sentence.append("to your cabinet.")
        self.slow_print(" ".join(sentence))

    def removing_from_cabinet(self, ingrs):
        """Reiterates the ingredients to be removed from the user's cabinet."""
        sentence = ["Alright I'll remove"]
        counter = 0
        for i_id in ingrs:
            counter += 1
            if (counter == len(ingrs)) and (len(ingrs) is not 1):
                sentence.append("and")
            if (counter is not len(ingrs)-1) and (counter is not len(ingrs)):
                sentence.append( ("%s,") % (self.db.getIngredientName(i_id)) )
            else:
                sentence.append(self.db.getIngredientName(i_id))
        sentence.append("from your cabinet.")
        self.slow_print( " ".join(sentence) )

    def change_of_interpretation(self):
        """If the DM descides that the user is wants something other than
            what was originally perceived, say this before branching to the new 
        (hopefully correct) state"""
        self.slow_print("Oh dear, I'm getting old...")

    def dont_know_that_drink(self):
        """Let the user know that we think they want a drink, but we don't recognize it"""
        self.slow_print("I'm sorry, I don't think I know that drink.")
        self.slow_print("What else can I do for you?")

    def duplicate_ingredients(self, ingrs):
        """Let user know that he/she can't add the same ingredients to the cabinet """
        sentence = ["I'm sorry, it looks like you already have"]
        counter = 0
        for i_id in ingrs:
            counter += 1
            if (counter == len(ingrs)) and (len(ingrs) is not 1):
                sentence.append("and")
            if (counter is not len(ingrs)-1) and (counter is not len(ingrs)):
                sentence.append( ("%s,") % (self.db.getIngredientName(i_id)) )
            else:
                sentence.append(self.db.getIngredientName(i_id))
        sentence.append("in your cabinet.")
        self.slow_print( " ".join(sentence) )
        
    def not_in_cabinet(self, ingrs):
        """Tell the user they can't remove ingredients not in their cabinet """
        sentence = ["I'm sorry, it looks like"]
        counter = 0
        for i_id in ingrs:
            counter += 1
            if (counter == len(ingrs)) and (len(ingrs) is not 1):
                sentence.append("and")
            if (counter is not len(ingrs)-1) and (counter is not len(ingrs)):
                sentence.append( ("%s,") % (self.db.getIngredientName(i_id)) )
            else:
                sentence.append(self.db.getIngredientName(i_id))
        if len(ingrs) == 1:
            sentence.append("isn't")
        else:
            sentence.append("aren't")
        sentence.append("in your cabinet.")
        self.slow_print( " ".join(sentence) )

    def confused(self):
        """Tell the user we don't understand what they said."""
        self.slow_print("\nHmmm I'm confused... Let's start over.")
        self.slow_print("What can I help you with?")

    def wtf(self):
        """We can't understand what the user is saying. Inform the user of this
           and that we are going to quit."""
        self.slow_print("\nOK, I really don't understand you... Have you" \
                        + " been drinking?")
        self.slow_print("I can't work like this!")
        
    def kbye(self,drink_id):
        '''The user asked for a drink that would kill them.
        Don't take it too serisouly.'''
        drinkname = self.db.getDrinkName(drink_id)
        ingredients = self.db.getIngredientAmount(drink_id)
        instructions = self.db.getInstructions(drink_id)
        self.slow_print("\nOkay, for a %s you're gonna want:\n" % (drinkname))
        for i in ingredients:
            self.slow_print(i)
        self.slow_print("\n"+instructions+"\n")
        self.slow_print("k bye!")

    def goodbye(self):
        """Prink a nice goodbye message."""
        print ""
        self.slow_print("Ok, see you later!")

    def slow_print(self, text):
        """Print text slowly, to look more human."""
        # print a character at a time
        for c in text:
            sleep(uniform(.001,.02))
            #sys.stdout.write(c)
            sys.stdout.write(c.encode('utf-8', 'ignore'))
            sys.stdout.flush()
        sys.stdout.write('\n')
        sys.stdout.flush()

    def look_busy(self):
        """Look busy in front of the user for a few seconds."""
        sleep(uniform(0,1.5))

    def welcome_to_pappa(self):
        print "8888888b.                                  888888b.             888 "      
        print "888   Y88b                                 888  *88b            888 "   
        print "888    888                                 888  .88P            888 "  
        print "888   d88P 8888b.    88888b.   8888b.      8888888K.   .d88b. 8888888  " 
        print "8888888P8     088b   888 888b     088b     888  YY88b d88..88b  888     "
        print "888       .d888888   888  888 .d888888 888 888    888 888  888  888      "
        print "888       888  888   888 d88P 888  888     888   d88P Y88..88P  Y88b.   "
        print "888       9Y888888   88888PP  8Y888888     8888888P8   8Y88P8   YY888   "
        print "                     888                                               "
        print "                     888                                               "
        print "                     888                                               "
        print "########################\n"
